4.4 匿名函数
匿名函数是指没有定义名字符号的函数。
除没有名字外,匿名函数和普通函数完全相同。最大区别是,我们可在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。
直接执行:
func main() {
func(s string) {
println(s)
}("hello,world!")
}赋值给变量:
func main() {
add:=func(x,y int)int{
return x+y
}
println(add(1,2))
}
作为参数:
func test(f func()) {
f()
}
func main() {
test(func() {
println("hello,world!")
})
}作为返回值:
func test() func(int,int)int{
return func(x,y int)int{
return x+y
}
}
func maidn() {
add:=test()
println(add(1,2))
}将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别。当然,编译器会为匿名函数生成一个“随机”符号名。
普通函数和匿名函数都可作为结构体字段,或经通道传递。
func testStruct() {
type calc struct{ // 定义结构体类型
mul func(x,y int)int // 函数类型字段
}
x:=calc{
mul:func(x,y int)int{
return x*y
},
}
println(x.mul(2,3))
}
func testChannel() {
c:=make(chan func(int,int)int,2)
c<-func(x,y int)int{
return x+y
}
println((<-c)(1,2))
}
不曾使用的匿名函数会被编译器当作错误。
func main() {
func(s string) { // 错误:func literal evaluated but not used
println(s)
} // 此处并未调用
}除闭包因素外,匿名函数也是一种常见重构手段。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。
相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码层次。
闭包
闭包(closure)是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。这种说明太学术范儿了,很难理解,我们先看一个例子。
func test(x int)func() {
return func() {
println(x)
}
}
func main() {
f:=test(123)
f()
}输出:
123
就这段代码而言,test返回的匿名函数会引用上下文环境变量x。当该函数在main中执行时,它依然可正确读取x的值,这种现象就称作闭包。
闭包是如何实现的?匿名函数被返回后,为何还能读取环境变量值?修改一下代码再看。
package main
func test(x int)func() {
println(&x)
return func() {
println(&x,x)
}
}
func main() {
f:=test(0x100)
f()
}输出:
0xc82000a100
0xc+82000a100 256
通过输出指针,我们注意到闭包直接引用了原环境变量。分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。所以说,闭包是函数和引用环境的组合体更加确切。
本质上返回的是一个funcval结构,可在runtime/runtime2.go中找到相关定义。
$go build-gcflags"-N-l" # 禁用内联和代码优化
$gdb test
(gdb)b 6 # 设置断点后,执行
(gdb)b 13
(gdb)r
(gdb)info locals # 进入test函数,获取环境变量x地址
&x=0xc82000a130
(gdb)c # 继续执行,回到main函数
(gdb)disas
Dump of assembler code for function main.main:
0x000000000000213f<+15>: sub rsp,0x18
0x0000000000002143<+19>: mov QWORD PTR[rsp],0x100
0x000000000000214b<+27>: call 0x2040<main.test>
0x0000000000002150<+32>: mov rbx,QWORD PTR[rsp+0x8] #test返回值
0x0000000000002155<+37>: mov QWORD PTR[rsp+0x10],rbx
=>0x000000000000215a<+42>: mov rbx,QWORD PTR[rsp+0x10]
0x000000000000215f<+47>: mov rdx,rbx # 保存到rdx寄存器
0x0000000000002162<+50>: mov rbx,QWORD PTR[rdx]
0x0000000000002165<+53>: call rbx
0x0000000000002167<+55>: add rsp,0x18
0x000000000000216b<+59>: ret
(gdb)x/2xg$rbx # 包含匿名函数和环境变量地址
0xc82000a140: 0x0000000000002180 0x000000c82000a130
(gdb)info symbol 0x0000000000002180
main.test.func1 in section.text
(gdb)s # 继续,进入匿名函数
Breakpoint 1,main.test.func1
(gdb)disas
Dump of assembler code for function main.test.func1:
0x000000000000218f<+15>: sub rsp,0x18
0x0000000000002193<+19>: mov rbx,QWORD PTR[rdx+0x8] # 经rdx读取环境变量地址
0x0000000000002197<+23>: mov QWORD PTR[rsp+0x10],rbx
0x000000000000219c<+28>: mov rbx,QWORD PTR[rsp+0x10]
0x00000000000021a1<+33>: mov QWORD PTR[rsp+0x8],rbx
0x00000000000021a6<+38>: call 0x23ac0<runtime.printlock>
0x00000000000021ab<+43>: mov rbx,QWORD PTR[rsp+0x8]
0x00000000000021b0<+48>: mov QWORD PTR[rsp],rbx
0x00000000000021b4<+52>: call 0x244a0<runtime.printpointer>
(gdb)x/1xg$rdx+0x8
0xc82000a148: 0x000000c82000a130
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性。
func test() []func() {
var s[]func()
for i:=0;i<2;i++ {
s=append(s,func() { // 将多个匿名函数添加到列表
println(&i,i)
})
}
return s // 返回匿名函数列表
}
func main() {
for_,f:=range test() { // 迭代执行所有匿名函数
f()
}
}输出:
0xc82000a078 2
0xc82000a078 2
对这个输出结果不必惊讶。很简单,for循环复用局部变量i,那么每次添加的匿名函数引用的自然是同一变量。添加操作仅仅是将匿名函数放入列表,并未执行。因此,当main执行这些函数时,它们读取的是环境变量i最后一次循环时的值。不是2,还能是什么?
解决方法就是每次用不同的环境变量或传参复制,让各自闭包环境各不相同。
func test() []func() {
var s[]func()
for i:=0;i<2;i++ {
x:=i //x每次循环都重新定义
s=append(s,func() {
println(&x,x)
})
}
return s
}输出:
0xc82006e000 0
0xc82006e008 1
多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数取值,在并发模式下可能需要做同步处理。
func test(x int) (func(),func()) { // 返回两个匿名函数
return func() {
println(x)
x+=10 // 修改环境变量
},func() {
println(x) // 显示环境变量
}
}
func main() {
a,b:=test(100)
a()
b()
}
输出:
100
110
闭包让我们不用传递参数就可读取或修改环境状态,当然也要为此付出额外代价。对于性能要求较高的场合,须慎重使用。